/*!
 *
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License, version 2 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 *
 * Copyright (c) 2002-2018 Hitachi Vantara. All rights reserved.
 *
 */

package org.pentaho.platform.repository2.unified.jcr;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.lock.Lock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.platform.api.locale.IPentahoLocale;
import org.pentaho.platform.api.repository2.unified.IRepositoryAccessVoterManager;
import org.pentaho.platform.api.repository2.unified.IRepositoryDefaultAclHandler;
import org.pentaho.platform.api.repository2.unified.IRepositoryFileData;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl;
import org.pentaho.platform.api.repository2.unified.RepositoryFilePermission;
import org.pentaho.platform.api.repository2.unified.RepositoryFileTree;
import org.pentaho.platform.api.repository2.unified.RepositoryRequest;
import org.pentaho.platform.api.repository2.unified.UnifiedRepositoryException;
import org.pentaho.platform.api.repository2.unified.VersionSummary;
import org.pentaho.platform.api.repository2.unified.data.node.DataNode;
import org.pentaho.platform.api.repository2.unified.data.node.NodeRepositoryFileData;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.repository2.messages.Messages;
import org.pentaho.platform.repository2.unified.IRepositoryFileAclDao;
import org.pentaho.platform.repository2.unified.IRepositoryFileDao;
import org.pentaho.platform.repository2.unified.ServerRepositoryPaths;
import org.springframework.extensions.jcr.JcrCallback;
import org.springframework.extensions.jcr.JcrTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * CRUD operations against JCR. Note that there is no access control in this class (implicit or explicit).
 * 
 * @author mlowery
 */
public class JcrRepositoryFileDao implements IRepositoryFileDao {
  private static final Log logger = LogFactory.getLog( JcrRepositoryFileDao.class );

  // ~ Static fields/initializers
  // ======================================================================================

  // ~ Instance fields
  // =================================================================================================
  private JcrTemplate jcrTemplate;

  private List<ITransformer<IRepositoryFileData>> transformers;

  private ILockHelper lockHelper;

  private IDeleteHelper deleteHelper;

  private IPathConversionHelper pathConversionHelper;

  private IRepositoryFileAclDao aclDao;

  private IRepositoryDefaultAclHandler defaultAclHandler;

  private IRepositoryAccessVoterManager accessVoterManager;

  // ~ Constructors
  // ====================================================================================================

  public JcrRepositoryFileDao( final JcrTemplate jcrTemplate,
      final List<ITransformer<IRepositoryFileData>> transformers, final ILockHelper lockHelper,
      final IDeleteHelper deleteHelper, final IPathConversionHelper pathConversionHelper,
      final IRepositoryFileAclDao aclDao, final IRepositoryDefaultAclHandler defaultAclHandler ) {
    super();
    Assert.notNull( jcrTemplate );
    Assert.notNull( transformers );
    this.jcrTemplate = jcrTemplate;
    this.transformers = transformers;
    this.lockHelper = lockHelper;
    this.deleteHelper = deleteHelper;
    this.pathConversionHelper = pathConversionHelper;
    this.aclDao = aclDao;
    this.defaultAclHandler = defaultAclHandler;
  }

  public JcrRepositoryFileDao( final JcrTemplate jcrTemplate,
      final List<ITransformer<IRepositoryFileData>> transformers, final ILockHelper lockHelper,
      final IDeleteHelper deleteHelper, final IPathConversionHelper pathConversionHelper,
      final IRepositoryFileAclDao aclDao, final IRepositoryDefaultAclHandler defaultAclHandler,
      final IRepositoryAccessVoterManager accessVoterManager ) {
    this( jcrTemplate, transformers, lockHelper, deleteHelper, pathConversionHelper, aclDao, defaultAclHandler );
    this.accessVoterManager = accessVoterManager;
  }

  // ~ Methods
  // =========================================================================================================

  private RepositoryFile internalCreateFolder( final Session session, final Serializable parentFolderId,
      final RepositoryFile folder, final RepositoryFileAcl acl, final String versionMessage )
    throws RepositoryException {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    // Get repository file info and acl info of parent
    if ( parentFolderId != null ) {
      RepositoryFile parentRepositoryFile = getFileById( parentFolderId );
      if ( parentRepositoryFile != null ) {
        RepositoryFileAcl parentAcl = aclDao.getAcl( parentRepositoryFile.getId() );
        // Invoke accessVoterManager to see if we have access to perform this operation
        if ( !accessVoterManager.hasAccess( parentRepositoryFile, RepositoryFilePermission.WRITE, parentAcl,
            PentahoSessionHolder.getSession() ) ) {
          return null;
        }
      }
    }
    PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
    JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId );
    Node folderNode = JcrRepositoryFileUtils.createFolderNode( session, pentahoJcrConstants, parentFolderId, folder );
    // create a temporary folder object with correct path for default acl purposes.
    String path = JcrRepositoryFileUtils.getAbsolutePath( session, pentahoJcrConstants, folderNode );
    RepositoryFile tmpFolder = new RepositoryFile.Builder( folder ).path( path ).build();
    // we must create the acl during checkout
    JcrRepositoryFileAclUtils.createAcl( session, pentahoJcrConstants, folderNode.getIdentifier(), acl == null
        ? defaultAclHandler.createDefaultAcl( tmpFolder ) : acl );
    session.save();
    if ( folder.isVersioned() ) {
      JcrRepositoryFileUtils.checkinNearestVersionableNodeIfNecessary( session, pentahoJcrConstants, folderNode,
          versionMessage );
    }
    JcrRepositoryFileUtils
        .checkinNearestVersionableFileIfNecessary(
            session,
            pentahoJcrConstants,
            parentFolderId,
            Messages
                .getInstance()
                .getString(
                    "JcrRepositoryFileDao.USER_0001_VER_COMMENT_ADD_FOLDER", folder.getName(), ( parentFolderId == null ? "root" : parentFolderId.toString() ) ) ); //$NON-NLS-1$ //$NON-NLS-2$
    return JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
        folderNode );
  }

  private RepositoryFile internalCreateFile( final Serializable parentFolderId, final RepositoryFile file,
      final IRepositoryFileData content, final RepositoryFileAcl acl, final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    /*
     * PPP-3049: Changed the Assert.notNull(content) to code that creates a file with a single blank when the assert
     * WOULD have been triggered.
     */
    Assert.notNull( file );
    Assert.isTrue( !file.isFolder() );

    // Get repository file info and acl info of parent
    if ( parentFolderId != null ) {
      RepositoryFile parentRepositoryFile = getFileById( parentFolderId );
      if ( parentRepositoryFile != null ) {
        RepositoryFileAcl parentAcl = aclDao.getAcl( parentRepositoryFile.getId() );
        // Invoke accessVoterManager to see if we have access to perform this operation
        if ( !accessVoterManager.hasAccess( parentRepositoryFile, RepositoryFilePermission.WRITE, parentAcl,
            PentahoSessionHolder.getSession() ) ) {
          return null;
        }
      }
    }
    // Assert.notNull(content);
    DataNode emptyDataNode = new DataNode( file.getName() );
    emptyDataNode.setProperty( " ", "content" ); //$NON-NLS-1$ //$NON-NLS-2$
    final IRepositoryFileData emptyContent = new NodeRepositoryFileData( emptyDataNode );

    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId );
        Node fileNode =
            JcrRepositoryFileUtils.createFileNode( session, pentahoJcrConstants, parentFolderId, file, content == null
                ? emptyContent : content, findTransformerForWrite( content == null ? emptyContent.getClass() : content
                .getClass() ) );
        // create a tmp file with correct path for default acl creation purposes.
        String path = JcrRepositoryFileUtils.getAbsolutePath( session, pentahoJcrConstants, fileNode );
        RepositoryFile tmpFile = new RepositoryFile.Builder( file ).path( path ).build();
        // we must create the acl during checkout
        aclDao.createAcl( fileNode.getIdentifier(), acl == null ? defaultAclHandler.createDefaultAcl( tmpFile ) : acl );
        session.save();
        if ( file.isVersioned() ) {
          JcrRepositoryFileUtils.checkinNearestVersionableNodeIfNecessary( session, pentahoJcrConstants, fileNode,
              versionMessage, file.getCreatedDate(), false );

        }
        JcrRepositoryFileUtils
            .checkinNearestVersionableFileIfNecessary(
                session,
                pentahoJcrConstants,
                parentFolderId,
                Messages
                    .getInstance()
                    .getString(
                        "JcrRepositoryFileDao.USER_0002_VER_COMMENT_ADD_FILE", file.getName(), ( parentFolderId == null ? "root" : parentFolderId.toString() ) ) ); //$NON-NLS-1$ //$NON-NLS-2$
        return JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
            fileNode );
      }
    } );
  }

  private RepositoryFile internalUpdateFile( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final RepositoryFile file, final IRepositoryFileData content, final String versionMessage )
    throws RepositoryException {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( file );
    Assert.isTrue( !file.isFolder() );
    Assert.notNull( content );
    // Get repository file info and acl info of parent
    RepositoryFileAcl acl = aclDao.getAcl( file.getId() );
    // Invoke accessVoterManager to see if we have access to perform this operation
    if ( !accessVoterManager.hasAccess( file, RepositoryFilePermission.WRITE, acl, PentahoSessionHolder.getSession() ) ) {
      return null;
    }
    lockHelper.addLockTokenToSessionIfNecessary( session, pentahoJcrConstants, file.getId() );
    JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, file.getId() );
    JcrRepositoryFileUtils.updateFileNode( session, pentahoJcrConstants, file, content,
        findTransformerForWrite( content.getClass() ) );
    session.save();
    JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, file.getId(),
        versionMessage, file.getCreatedDate() != null ? file.getCreatedDate() : new java.util.Date(), true );
    lockHelper.removeLockTokenFromSessionIfNecessary( session, pentahoJcrConstants, file.getId() );
    return JcrRepositoryFileUtils.nodeIdToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper, file
        .getId() );
  }

  private RepositoryFile internalUpdateFolder( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final RepositoryFile folder, final String versionMessage ) throws RepositoryException {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( folder );
    Assert.isTrue( folder.isFolder() );
    lockHelper.addLockTokenToSessionIfNecessary( session, pentahoJcrConstants, folder.getId() );
    JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, folder.getId() );
    JcrRepositoryFileUtils.updateFolderNode( session, pentahoJcrConstants, folder );
    session.save();
    JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, folder.getId(),
        versionMessage );
    lockHelper.removeLockTokenFromSessionIfNecessary( session, pentahoJcrConstants, folder.getId() );
    return JcrRepositoryFileUtils.nodeIdToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper, folder
        .getId() );
  }

  protected ITransformer<IRepositoryFileData> findTransformerForRead( final String contentType,
      final Class<? extends IRepositoryFileData> clazz ) {
    for ( ITransformer<IRepositoryFileData> transformer : transformers ) {
      if ( transformer.canRead( contentType, clazz ) ) {
        return transformer;
      }
    }
    throw new IllegalArgumentException( Messages.getInstance().getString(
        "JcrRepositoryFileDao.ERROR_0001_NO_TRANSFORMER" ) ); //$NON-NLS-1$
  }

  protected ITransformer<IRepositoryFileData> findTransformerForWrite(
    Class<? extends IRepositoryFileData> clazz ) {

    for ( ITransformer<IRepositoryFileData> transformer : transformers ) {
      if ( transformer.canWrite( clazz ) ) {
        return transformer;
      }
    }
    throw new IllegalArgumentException( Messages.getInstance().getString(
      "JcrRepositoryFileDao.ERROR_0001_NO_TRANSFORMER" ) ); //$NON-NLS-1$
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile createFile( final Serializable parentFolderId, final RepositoryFile file,
      final IRepositoryFileData content, final RepositoryFileAcl acl, final String versionMessage ) {
    return internalCreateFile( parentFolderId, file, content, acl, versionMessage );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile createFolder( final Serializable parentFolderId, final RepositoryFile folder,
      final RepositoryFileAcl acl, final String versionMessage ) {
    Assert.notNull( folder );
    Assert.isTrue( folder.isFolder() );

    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        return internalCreateFolder( session, parentFolderId, folder, acl, versionMessage );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile getFileById( final Serializable fileId ) {
    return internalGetFileById( fileId, false, null );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile getFileById( final Serializable fileId, final boolean loadMaps ) {
    return internalGetFileById( fileId, loadMaps, null );
  }

  @Override
  public RepositoryFile getFile( final String relPath, final IPentahoLocale locale ) {
    return getFile( relPath, false, locale );
  }

  @Override
  public RepositoryFile getFileById( Serializable fileId, IPentahoLocale locale ) {
    return internalGetFileById( fileId, false, locale );
  }

  @Override
  public RepositoryFile getFile( final String relPath, final boolean loadLocaleMaps, final IPentahoLocale locale ) {
    Assert.hasText( relPath );
    Assert.isTrue( relPath.startsWith( RepositoryFile.SEPARATOR ) );

    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        String absPath = pathConversionHelper.relToAbs( relPath );
        return internalGetFile( session, absPath, loadLocaleMaps, locale );
      }
    } );
  }

  @Override
  public RepositoryFile getFileById( Serializable fileId, boolean loadLocaleMaps, IPentahoLocale locale ) {
    return internalGetFileById( fileId, loadLocaleMaps, locale );
  }

  private RepositoryFile internalGetFileById( final Serializable fileId, final boolean loadMaps,
      final IPentahoLocale locale ) {
    Assert.notNull( fileId );
    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        Node fileNode;
        try {
          fileNode = session.getNodeByIdentifier( fileId.toString() );
        } catch ( ItemNotFoundException e ) {
          logger.info( "Couldn't find file by id: " + fileId );
          fileNode = null;
        }
        RepositoryFile file =
            fileNode != null ? JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper,
                lockHelper, fileNode, loadMaps, locale ) : null;
        if ( file != null ) {
          RepositoryFileAcl acl = aclDao.getAcl( file.getId() );
          // Invoke accessVoterManager to see if we have access to perform this operation
          if ( !accessVoterManager.hasAccess( file, RepositoryFilePermission.READ, acl, PentahoSessionHolder
              .getSession() ) ) {
            return null;
          }
        }
        return file;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile getFile( final String relPath ) {
    return getFile( relPath, false, null );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile getFileByAbsolutePath( final String absPath ) {
    Assert.hasText( absPath );
    Assert.isTrue( absPath.startsWith( RepositoryFile.SEPARATOR ) );
    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        return internalGetFile( session, absPath, false, null );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile getFile( final String relPath, final boolean loadMaps ) {
    Assert.hasText( relPath );
    Assert.isTrue( relPath.startsWith( RepositoryFile.SEPARATOR ) );
    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        String absPath = pathConversionHelper.relToAbs( relPath );
        return internalGetFile( session, absPath, loadMaps, null );
      }
    } );
  }

  private RepositoryFile internalGetFile( final Session session, final String absPath, final boolean loadMaps,
      final IPentahoLocale locale ) throws RepositoryException {

    PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
    Item fileNode;
    try {
      fileNode = session.getItem( JcrStringHelper.pathEncode( absPath ) );
      // items are nodes or properties; this must be a node
      Assert.isTrue( fileNode.isNode() );
    } catch ( PathNotFoundException e ) {
      fileNode = null;
    }
    RepositoryFile file =
        fileNode != null ? JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper,
            lockHelper, (Node) fileNode, loadMaps, locale ) : null;
    if ( file != null ) {
      RepositoryFileAcl acl = aclDao.getAcl( file.getId() );
      // Invoke accessVoterManager to see if we have access to perform this operation
      if ( !accessVoterManager.hasAccess( file, RepositoryFilePermission.READ, acl, PentahoSessionHolder.getSession() ) ) {
        return null;
      }
    }
    return file;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings( "unchecked" )
  public <T extends IRepositoryFileData> T getData( final Serializable fileId, final Serializable versionId,
      final Class<T> contentClass ) {
    Assert.notNull( fileId );
    return (T) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        IRepositoryFileData data =
            JcrRepositoryFileUtils.getContent( session, pentahoJcrConstants, fileId, versionId, findTransformerForRead(
                JcrRepositoryFileUtils.getFileContentType( session, pentahoJcrConstants, fileId, versionId ),
                contentClass ) );
        if ( fileId != null ) {
          RepositoryFile file = internalGetFileById( fileId, false, null );
          if ( file != null ) {
            RepositoryFileAcl acl = aclDao.getAcl( fileId );
            // Invoke accessVoterManager to see if we have access to perform this operation
            if ( !accessVoterManager.hasAccess( file, RepositoryFilePermission.READ, acl, PentahoSessionHolder
                .getSession() ) ) {
              return null;
            }
          }
        }
        return data;
      }
    } );

  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings( "unchecked" )
  public List<RepositoryFile> getChildren( final RepositoryRequest repositoryRequest ) {
    Assert.notNull( repositoryRequest.getPath() );
    return (List<RepositoryFile>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return JcrRepositoryFileUtils.getChildren( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
            repositoryRequest );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings( "unchecked" )
  public List<RepositoryFile> getChildren( final Serializable folderId, final String filter,
      final Boolean showHiddenFiles ) {
    Assert.notNull( folderId );
    return (List<RepositoryFile>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return JcrRepositoryFileUtils.getChildren( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
            folderId, filter, showHiddenFiles );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile updateFile( final RepositoryFile file, final IRepositoryFileData content,
      final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( file );
    Assert.isTrue( !file.isFolder() );
    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return internalUpdateFile( session, pentahoJcrConstants, file, content, versionMessage );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void lockFile( final Serializable fileId, final String message ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        lockHelper.lockFile( session, pentahoJcrConstants, fileId, message );
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void unlockFile( final Serializable fileId ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        lockHelper.unlockFile( session, pentahoJcrConstants, fileId );
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings( "unchecked" )
  public List<VersionSummary> getVersionSummaries( final Serializable fileId ) {
    Assert.notNull( fileId );
    return (List<VersionSummary>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return JcrRepositoryFileUtils.getVersionSummaries( session, pentahoJcrConstants, fileId, true );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFile getFile( final Serializable fileId, final Serializable versionId ) {
    Assert.notNull( fileId );
    Assert.notNull( versionId );
    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return JcrRepositoryFileUtils.getFileAtVersion( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
            fileId, versionId );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void deleteFile( final Serializable fileId, final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        RepositoryFile fileToBeDeleted = getFileById( fileId );
        // Get repository file info and acl info of parent
        if ( fileToBeDeleted != null ) {
          RepositoryFileAcl toBeDeletedFileAcl = aclDao.getAcl( fileToBeDeleted.getId() );
          // Invoke accessVoterManager to see if we have access to perform this operation
          if ( !accessVoterManager.hasAccess( fileToBeDeleted, RepositoryFilePermission.DELETE, toBeDeletedFileAcl,
                  PentahoSessionHolder.getSession() ) ) {
            return null;
          }

          List<RepositoryFilePermission> perms = new ArrayList<RepositoryFilePermission>();
          perms.add( RepositoryFilePermission.DELETE );
          if ( !aclDao.hasAccess( fileToBeDeleted.getPath(), EnumSet.copyOf( perms ) ) ) {
            throw new AccessDeniedException( Messages.getInstance().getString(
                    "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED_DELETE", fileId ) );
          }
        }
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        Serializable parentFolderId = JcrRepositoryFileUtils.getParentId( session, fileId );
        JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId );
        deleteHelper.deleteFile( session, pentahoJcrConstants, fileId );
        session.save();
        JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId,
            versionMessage );
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void deleteFileAtVersion( final Serializable fileId, final Serializable versionId ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    Assert.notNull( versionId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        RepositoryFile fileToBeDeleted = getFileById( fileId );
        // Get repository file info and acl info of parent
        if ( fileToBeDeleted != null ) {
          RepositoryFileAcl toBeDeletedFileAcl = aclDao.getAcl( fileToBeDeleted.getId() );
          // Invoke accessVoterManager to see if we have access to perform this operation
          if ( !accessVoterManager.hasAccess( fileToBeDeleted, RepositoryFilePermission.DELETE, toBeDeletedFileAcl,
              PentahoSessionHolder.getSession() ) ) {
            return null;
          }
        }
        Node fileToDeleteNode = session.getNodeByIdentifier( fileId.toString() );
        session.getWorkspace().getVersionManager().getVersionHistory( fileToDeleteNode.getPath() ).removeVersion(
            versionId.toString() );
        session.save();
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings( "unchecked" )
  public List<RepositoryFile> getDeletedFiles( final String origParentFolderPath, final String filter ) {
    Assert.hasLength( origParentFolderPath );
    return (List<RepositoryFile>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return deleteHelper.getDeletedFiles( session, pentahoJcrConstants, origParentFolderPath, filter );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings( "unchecked" )
  public List<RepositoryFile> getDeletedFiles() {
    return (List<RepositoryFile>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return deleteHelper.getDeletedFiles( session, pentahoJcrConstants );
      }
    } );
  }

  @Override
  @SuppressWarnings( "unchecked" )
  public List<RepositoryFile> getAllDeletedFiles() {
    return (List<RepositoryFile>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return deleteHelper.getAllDeletedFiles( session, pentahoJcrConstants );
      }
    } );
  }

  /**
   * {@inheritDoc}
   * <p/>
   * <p>
   * No checkout needed as .trash is not versioned.
   * </p>
   */
  @Override
  public void permanentlyDeleteFile( final Serializable fileId, final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        RepositoryFile fileToBeDeleted = getFileById( fileId );
        // Get repository file info and acl info of parent
        if ( fileToBeDeleted != null ) {
          RepositoryFileAcl toBeDeletedFileAcl = aclDao.getAcl( fileToBeDeleted.getId() );
          // Invoke accessVoterManager to see if we have access to perform this operation
          if ( !accessVoterManager.hasAccess( fileToBeDeleted, RepositoryFilePermission.DELETE, toBeDeletedFileAcl,
              PentahoSessionHolder.getSession() ) ) {
            return null;
          }
        }
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        deleteHelper.permanentlyDeleteFile( session, pentahoJcrConstants, fileId );
        session.save();
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void undeleteFile( final Serializable fileId, final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        String absOrigParentFolderPath =
            deleteHelper.getOriginalParentFolderPath( session, pentahoJcrConstants, fileId );
        Serializable origParentFolderId = null;
        RepositoryFile file = getFileById( fileId );
        RepositoryFileAcl acl = aclDao.getAcl( fileId );
        if ( !accessVoterManager.hasAccess( file, RepositoryFilePermission.WRITE, acl, PentahoSessionHolder
            .getSession() ) ) {
          return null;
        }
        // original parent folder path may no longer exist!
        if ( session.itemExists( JcrStringHelper.pathEncode( absOrigParentFolderPath ) ) ) {
          origParentFolderId =
              ( (Node) session.getItem( JcrStringHelper.pathEncode( absOrigParentFolderPath ) ) ).getIdentifier();
        } else {
          // go through each of the segments of the original parent folder path, creating as necessary
          String[] segments = pathConversionHelper.absToRel( absOrigParentFolderPath ).split( RepositoryFile.SEPARATOR );
          RepositoryFile lastParentFolder =
              internalGetFile( session, ServerRepositoryPaths.getTenantRootFolderPath(), false, null );
          for ( String segment : segments ) {
            if ( StringUtils.hasLength( segment ) ) {
              RepositoryFile tmp =
                  internalGetFile( session, pathConversionHelper
                      .relToAbs( ( lastParentFolder.getPath().equals( RepositoryFile.SEPARATOR )
                          ? "" : lastParentFolder.getPath() ) + RepositoryFile.SEPARATOR + segment ), false, null ); //$NON-NLS-1$
              if ( tmp == null ) {
                lastParentFolder =
                    internalCreateFolder( session, lastParentFolder.getId(), new RepositoryFile.Builder( segment )
                        .folder( true ).build(), defaultAclHandler.createDefaultAcl( lastParentFolder ), null );
              } else {
                lastParentFolder = tmp;
              }
            }
          }
          origParentFolderId = lastParentFolder.getId();
        }
        JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants,
            origParentFolderId );
        deleteHelper.undeleteFile( session, pentahoJcrConstants, fileId );
        session.save();
        JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants,
            origParentFolderId, versionMessage );
        return null;
      }
    } );
  }

  private void internalCopyOrMove( final Serializable fileId, final String destRelPath, final String versionMessage,
      final boolean copy ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        // if we're moving the file,
        // check that user has permissions to remove the file from it's current location
        RepositoryFile file = getFileById( fileId );
        if ( !copy ) {
          RepositoryFileAcl acl = aclDao.getAcl( fileId );
          if ( !accessVoterManager.hasAccess( file, RepositoryFilePermission.WRITE, acl,
            PentahoSessionHolder.getSession() ) ) {
            throw new AccessDeniedException( Messages.getInstance().getString(
              "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED_DELETE", fileId ) );
          }
        }

        // check that user has permissions to write to the destination folder
        RepositoryFile destFolder = getFile( destRelPath );
        if ( destFolder != null ) {
          RepositoryFileAcl destFolderAcl = aclDao.getAcl( destFolder.getId() );
          if ( !accessVoterManager.hasAccess( destFolder, RepositoryFilePermission.WRITE, destFolderAcl,
            PentahoSessionHolder.getSession() ) ) {
            throw new AccessDeniedException( Messages.getInstance().getString(
              "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED_CREATE", destFolder.getId() ) );
          }
        }

        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        String destAbsPath = pathConversionHelper.relToAbs( destRelPath );
        String cleanDestAbsPath = destAbsPath;
        if ( cleanDestAbsPath.endsWith( RepositoryFile.SEPARATOR ) ) {
          cleanDestAbsPath.substring( 0, cleanDestAbsPath.length() - 1 );
        }
        Node srcFileNode = session.getNodeByIdentifier( fileId.toString() );
        Serializable srcParentFolderId = JcrRepositoryFileUtils.getParentId( session, fileId );
        boolean appendFileName = false;
        boolean destExists = true;
        Node destFileNode = null;
        Node destParentFolderNode = null;
        try {
          destFileNode = (Node) session.getItem( JcrStringHelper.pathEncode( cleanDestAbsPath ) );
        } catch ( PathNotFoundException e ) {
          destExists = false;
        }
        if ( destExists ) {
          // make sure it's a file or folder
          Assert.isTrue( JcrRepositoryFileUtils.isSupportedNodeType( pentahoJcrConstants, destFileNode ) );
          // existing item; make sure src is not a folder if dest is a file
          Assert.isTrue(
              !( JcrRepositoryFileUtils.isPentahoFolder( pentahoJcrConstants, srcFileNode ) && JcrRepositoryFileUtils
                  .isPentahoFile( pentahoJcrConstants, destFileNode ) ), Messages.getInstance().getString(
                  "JcrRepositoryFileDao.ERROR_0002_CANNOT_OVERWRITE_FILE_WITH_FOLDER" ) ); //$NON-NLS-1$
          if ( JcrRepositoryFileUtils.isPentahoFolder( pentahoJcrConstants, destFileNode ) ) {
            // existing item; caller is not renaming file, only moving it
            appendFileName = true;
            destParentFolderNode = destFileNode;
          } else {
            // get parent of existing dest item
            int lastSlashIndex = cleanDestAbsPath.lastIndexOf( RepositoryFile.SEPARATOR );
            Assert.isTrue( lastSlashIndex > 1, Messages.getInstance().getString(
                "JcrRepositoryFileDao.ERROR_0003_ILLEGAL_DEST_PATH" ) ); //$NON-NLS-1$
            String absPathToDestParentFolder = cleanDestAbsPath.substring( 0, lastSlashIndex );
            destParentFolderNode = (Node) session.getItem( JcrStringHelper.pathEncode( absPathToDestParentFolder ) );
          }
        } else {
          // destination doesn't exist; go up one level to a folder that does exist
          int lastSlashIndex = cleanDestAbsPath.lastIndexOf( RepositoryFile.SEPARATOR );
          Assert.isTrue( lastSlashIndex > 1, Messages.getInstance().getString(
              "JcrRepositoryFileDao.ERROR_0003_ILLEGAL_DEST_PATH" ) ); //$NON-NLS-1$
          String absPathToDestParentFolder = cleanDestAbsPath.substring( 0, lastSlashIndex );
          // Not need to check the name if we encoded it
          // JcrRepositoryFileUtils.checkName( cleanDestAbsPath.substring( lastSlashIndex + 1 ) );
          try {
            destParentFolderNode = (Node) session.getItem( JcrStringHelper.pathEncode( absPathToDestParentFolder ) );
          } catch ( PathNotFoundException e1 ) {
            Assert.isTrue( false, Messages.getInstance()
                .getString( "JcrRepositoryFileDao.ERROR_0004_PARENT_MUST_EXIST" ) ); //$NON-NLS-1$
          }
          Assert.isTrue( JcrRepositoryFileUtils.isPentahoFolder( pentahoJcrConstants, destParentFolderNode ), Messages
              .getInstance().getString( "JcrRepositoryFileDao.ERROR_0005_PARENT_MUST_BE_FOLDER" ) ); //$NON-NLS-1$
        }
        if ( !copy ) {
          JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants,
              srcParentFolderId );
        }
        JcrRepositoryFileUtils.checkoutNearestVersionableNodeIfNecessary( session, pentahoJcrConstants,
            destParentFolderNode );
        String finalEncodedSrcAbsPath = srcFileNode.getPath();
        String finalEncodedDestAbsPath = null;
        if ( appendFileName ) {
          final String fileName = srcFileNode.getName();
          if ( JcrStringHelper.isEncoded( fileName ) ) {
            finalEncodedDestAbsPath = JcrStringHelper.pathEncode( cleanDestAbsPath ) + RepositoryFile.SEPARATOR + fileName;
          } else {
            finalEncodedDestAbsPath = JcrStringHelper.pathEncode( cleanDestAbsPath + RepositoryFile.SEPARATOR + fileName );
          }
        } else {
          finalEncodedDestAbsPath = JcrStringHelper.pathEncode( cleanDestAbsPath );
        }
        try {
          if ( copy ) {
            session.getWorkspace().copy( finalEncodedSrcAbsPath, finalEncodedDestAbsPath );
          } else {
            session.getWorkspace().move( finalEncodedSrcAbsPath, finalEncodedDestAbsPath );
          }
        } catch ( ItemExistsException iae ) {
          throw new UnifiedRepositoryException( ( file.isFolder() ? "Folder " : "File " ) + "with path ["
              + cleanDestAbsPath + "] already exists in the repository" );
        }

        JcrRepositoryFileUtils.checkinNearestVersionableNodeIfNecessary( session, pentahoJcrConstants,
            destParentFolderNode, versionMessage );
        // if it's a move within the same folder, then the next checkin is unnecessary
        if ( !copy && !destParentFolderNode.getIdentifier().equals( srcParentFolderId.toString() ) ) {
          JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants,
              srcParentFolderId, versionMessage );
        }
        session.save();
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void moveFile( final Serializable fileId, final String destRelPath, final String versionMessage ) {
    internalCopyOrMove( fileId, destRelPath, versionMessage, false );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void copyFile( final Serializable fileId, final String destRelPath, final String versionMessage ) {
    internalCopyOrMove( fileId, destRelPath, versionMessage, true );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public VersionSummary getVersionSummary( final Serializable fileId, final Serializable versionId ) {
    Assert.notNull( fileId );
    return (VersionSummary) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return JcrRepositoryFileUtils.getVersionSummary( session, pentahoJcrConstants, fileId, versionId );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void restoreFileAtVersion( final Serializable fileId, final Serializable versionId,
                                    final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException(
        Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }
    Assert.notNull( fileId );
    Assert.notNull( versionId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        Node fileNode = session.getNodeByIdentifier( fileId.toString() );
        session.getWorkspace().getVersionManager().restore( fileNode.getPath(), versionId.toString(), true );
        return null;
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean canUnlockFile( final Serializable fileId ) {
    return (Boolean) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        Node fileNode = session.getNodeByIdentifier( fileId.toString() );
        Lock lock = session.getWorkspace().getLockManager().getLock( fileNode.getPath() );
        return lockHelper.canUnlock( session, pentahoJcrConstants, lock );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public RepositoryFileTree getTree( final RepositoryRequest repositoryRequest ) {
    Assert.hasText( repositoryRequest.getPath() );
    return (RepositoryFileTree) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        String absPath = pathConversionHelper.relToAbs( repositoryRequest.getPath() );
        return JcrRepositoryFileUtils.getTree( session, pentahoJcrConstants, pathConversionHelper, lockHelper, absPath,
            repositoryRequest, accessVoterManager );
      }
    } );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @Deprecated
  public RepositoryFileTree getTree( final String relPath, final int depth, final String filter,
      final boolean showHidden ) {
    Assert.hasText( relPath );
    final RepositoryRequest repositoryRequest = new RepositoryRequest( relPath, showHidden, depth, filter );
    return (RepositoryFileTree) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        String absPath = pathConversionHelper.relToAbs( relPath );
        return JcrRepositoryFileUtils.getTree( session, pentahoJcrConstants, pathConversionHelper, lockHelper, absPath,
            repositoryRequest, accessVoterManager );
      }
    } );
  }

  @Override
  @SuppressWarnings( "unchecked" )
  public List<RepositoryFile> getReferrers( final Serializable fileId ) {
    if ( fileId == null ) {
      return new ArrayList<RepositoryFile>();
    }

    return (List<RepositoryFile>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );

        Node fileNode = session.getNodeByIdentifier( fileId.toString() );
        // guard against using a file retrieved from a more lenient session inside a more strict session
        Assert.notNull( fileNode );

        Set<RepositoryFile> referrers = new HashSet<RepositoryFile>();
        PropertyIterator refIter = fileNode.getReferences();
        if ( refIter.hasNext() ) {
          while ( refIter.hasNext() ) {
            // for each referrer property, march up the tree until we find the file node to which the property
            // belongs
            RepositoryFile referrer = getReferrerFile( session, pentahoJcrConstants, refIter.nextProperty() );
            if ( referrer != null ) {
              referrers.add( referrer );
            }
          }
        }
        session.save();
        return Arrays.asList( referrers.toArray() );
      }
    } );
  }

  protected RepositoryFile getReferrerFile( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final Property referrerProperty ) throws RepositoryException {
    Node currentNode = referrerProperty.getParent();
    while ( !currentNode.isNodeType( pentahoJcrConstants.getPHO_NT_PENTAHOHIERARCHYNODE() ) ) {
      currentNode = currentNode.getParent();
    }
    // if folder, then referrer is a lock token record (under the user's home folder) which will be cleaned up by
    // lockHelper; ignore it
    if ( currentNode.isNodeType( pentahoJcrConstants.getPHO_NT_PENTAHOFOLDER() ) ) {
      return null;
    }

    return JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper,
        currentNode );
  }

  @Override
  public void setFileMetadata( final Serializable fileId, final Map<String, Serializable> metadataMap ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }
    Assert.notNull( fileId );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        JcrRepositoryFileUtils.setFileMetadata( session, fileId, metadataMap );
        return null;
      }
    } );
  }

  @Override
  @SuppressWarnings( "unchecked" )
  public Map<String, Serializable> getFileMetadata( final Serializable fileId ) {
    Assert.notNull( fileId );
    return (Map<String, Serializable>) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( Session session ) throws IOException, RepositoryException {
        return JcrRepositoryFileUtils.getFileMetadata( session, fileId );
      }
    } );
  }

  @Override
  public List<Character> getReservedChars() {
    return JcrRepositoryFileUtils.getReservedChars();
  }

  public IRepositoryDefaultAclHandler getDefaultAclHandler() {
    return defaultAclHandler;
  }

  public void setDefaultAclHandler( IRepositoryDefaultAclHandler defaultAclHandler ) {
    this.defaultAclHandler = defaultAclHandler;
  }

  @Override
  public List<Locale> getAvailableLocalesForFileById( Serializable fileId ) {
    RepositoryFile repositoryFile = getFileById( fileId, true );
    return getAvailableLocalesForFile( repositoryFile );
  }

  @Override
  public List<Locale> getAvailableLocalesForFileByPath( String relPath ) {
    RepositoryFile repositoryFile = getFileById( relPath, true );
    return getAvailableLocalesForFile( repositoryFile );
  }

  @Override
  public List<Locale> getAvailableLocalesForFile( RepositoryFile repositoryFile ) {
    List<Locale> localeList = new ArrayList<Locale>();
    if ( repositoryFile != null && repositoryFile.getLocalePropertiesMap() != null ) {
      for ( String localeName : repositoryFile.getLocalePropertiesMap().keySet() ) {
        String[] localePieces = localeName.split( "_" );
        String language = localePieces[0];
        String country = ( localePieces.length > 1 ) ? localePieces[1] : "";
        String variant = ( localePieces.length > 2 ) ? localePieces[2] : "";
        Locale locale = new Locale( language, country, variant );
        localeList.add( locale );
      }
    }

    return localeList;
  }

  @Override
  public Properties getLocalePropertiesForFileById( Serializable fileId, String locale ) {
    RepositoryFile repositoryFile = getFileById( fileId, true );
    return getLocalePropertiesForFile( repositoryFile, locale );
  }

  @Override
  public Properties getLocalePropertiesForFileByPath( String relPath, String locale ) {

    RepositoryFile repositoryFile = getFileById( relPath, true );
    return getLocalePropertiesForFile( repositoryFile, locale );
  }

  @Override
  public Properties getLocalePropertiesForFile( RepositoryFile repositoryFile, String locale ) {
    if ( org.apache.commons.lang.StringUtils.isBlank( locale ) ) {
      locale = RepositoryFile.DEFAULT_LOCALE;
    }
    if ( repositoryFile != null && repositoryFile.getLocalePropertiesMap() != null ) {
      Properties properties = repositoryFile.getLocalePropertiesMap().get( locale );
      return properties;
    }

    return null;
  }

  @Override
  public void setLocalePropertiesForFileById( Serializable fileId, String locale, Properties properties ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    RepositoryFile repositoryFile = getFileById( fileId, true );
    setLocalePropertiesForFile( repositoryFile, locale, properties );
  }

  @Override
  public void setLocalePropertiesForFileByPath( String relPath, String locale, Properties properties ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    RepositoryFile repositoryFile = getFileById( relPath, true );
    setLocalePropertiesForFile( repositoryFile, locale, properties );
  }

  @Override
  public void setLocalePropertiesForFile( final RepositoryFile repositoryFile, final String locale,
      final Properties properties ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( repositoryFile );
    Assert.notNull( locale );
    Assert.notNull( properties );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        String versionMessage =
            Messages.getInstance().getString( "JcrRepositoryFileDao.LOCALE_0001_UPDATE_PROPERTIES",
                repositoryFile.getId() );
        lockHelper.addLockTokenToSessionIfNecessary( session, pentahoJcrConstants, repositoryFile.getId() );
        JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, repositoryFile
            .getId() );
        JcrRepositoryFileUtils.updateFileLocaleProperties( session, repositoryFile.getId(), locale, properties );
        session.save();
        JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, repositoryFile
            .getId(), versionMessage );
        lockHelper.removeLockTokenFromSessionIfNecessary( session, pentahoJcrConstants, repositoryFile.getId() );
        return null;
      }
    } );
  }

  @Override
  public void deleteLocalePropertiesForFile( final RepositoryFile repositoryFile, final String locale ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( repositoryFile );
    Assert.notNull( locale );
    jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        String versionMessage =
            Messages.getInstance().getString( "JcrRepositoryFileDao.LOCALE_0002_DELETE_PROPERTIES",
                repositoryFile.getId() );
        lockHelper.addLockTokenToSessionIfNecessary( session, pentahoJcrConstants, repositoryFile.getId() );
        JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, repositoryFile
            .getId() );
        JcrRepositoryFileUtils.deleteFileLocaleProperties( session, repositoryFile.getId(), locale );
        session.save();
        JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, repositoryFile
            .getId(), versionMessage );
        lockHelper.removeLockTokenFromSessionIfNecessary( session, pentahoJcrConstants, repositoryFile.getId() );
        return null;
      }
    } );
  }

  @Override
  public RepositoryFile updateFolder( final RepositoryFile file, final String versionMessage ) {
    if ( isKioskEnabled() ) {
      throw new RuntimeException( Messages.getInstance().getString( "JcrRepositoryFileDao.ERROR_0006_ACCESS_DENIED" ) ); //$NON-NLS-1$
    }

    Assert.notNull( file );
    Assert.isTrue( file.isFolder() );
    return (RepositoryFile) jcrTemplate.execute( new JcrCallback() {
      @Override
      public Object doInJcr( final Session session ) throws RepositoryException, IOException {
        PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
        return internalUpdateFolder( session, pentahoJcrConstants, file, versionMessage );
      }
    } );
  }

  private String extractNameFromPath( String path ) {
    int startIndex = path.lastIndexOf( RepositoryFile.SEPARATOR );
    if ( startIndex >= 0 ) {
      int endIndex = path.indexOf( '.', startIndex );
      if ( endIndex > startIndex ) {
        return path.substring( startIndex + 1, endIndex );
      }
    }
    return null;
  }

  private boolean isKioskEnabled() {
    if ( PentahoSystem.getInitializedOK() ) {
      return "true".equals( PentahoSystem.getSystemSetting( "kiosk-mode", "false" ) );
    } else {
      return false;
    }
  }
}
